spring-framework-4-reference

9.2 关于 @AspectJ 的支持

@AspectJ 可以将切面声明为普通的 Java 类。 AspectJ 5发布的 AspectJ project 中引入了这种 @AspectJ 风格。Spring 使用了和 AspectJ 5 一样的注解,使用了 AspectJ 提供的一个库来做 pointcut(切点)解析和匹配。 但是,AOP 在运行时仍旧是纯的 Spring AOP,并不依赖于 AspectJ 的编译器或者 weaver(织入器)。

使用 AspectJ 的编译器或者 weaver(织入器)的话就可以使用完整的AspectJ 语言,我们将在9.8. Using AspectJ with Spring applications中讨论这个问题

9.2.1 启用 @AspectJ 支持

为了在 Spring 配置中使用 @AspectJ aspects,你必须首先启用Spring 对基于 @AspectJ aspects 的配置支持,autoproxying(自动代理)基于通知是否来自这些切面。 自动代理是指 Spring 会判断一个bean 是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确认通知是否如期进行。

通过 XML 或者 Java 配置来 启用 @AspectJ 支持。在任何情况下,你还需要确保 AspectJ aspectjweaver.jar在你的应用程序的类路径中(版本 1.6.8 或以后)。这个库在 AspectJ 发布的 lib 目录中或通过Maven 的 中央存库得到。

用 Java 配置

使用 @Configuration 和 @EnableAspectJAutoProxy 注解:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

使用 XML 配置

使用 aop:aspectj-autoproxy 元素:

<aop:aspectj-autoproxy/>

9.2.2 声明一个切面

在启用 @AspectJ 支持的情况下,在application context中定义的任意带有一个 @Aspect 切面(拥有 @Aspect 注解)的 bean 都将被Spring自动识别并用于配置在 Spring AOP。 以下例子展示了为了完成一个不是非常有用的切面所需要的最小定义:

下面是在application context 中的一个常见的 bean 定义,这个 bean 指向一个使用了 @Aspect 注解的 bean 类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of aspect here as normal -->
</bean>

下面是 NotVeryUsefulAspect 类定义,使用了 org.aspectj.lang.annotation.Aspect 注解。

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

切面(用 @Aspect 注解的类)和其他类一样有方法和字段定义。他们也可能包括切入点,通知和引入(inter-type)声明。

你可以注册 切面 像其他 bean 一样在 Spring 的 XML 进行配置,或自动通过类路径扫描-就像任何其他的 Spring 管理的 bean。然而,请注意,@Aspect 的注解是不足以在 classpath 自动检测到了:为了这个目的,你需要添加一个单独的 @Component 的注解(或者自定义stereotype annotation 即 qualifies,作为 Spring 组件的扫描规则)。

在 Spring AOP 中,它是不可能使自己的切面成为 其他切面的通知的目标的。类上的 @Aspect 注解标记 它成为一个切面,因此从 auto-proxying 中排除了它。

9.2.3 声明一个切入点

回想一下,切入点决定了连接点关注的内容,使得我们可以控制通知什么执行。 Spring AOP 只支持 Spring bean 方法执行连接点。所以你可以把切入点看做是匹配 Spring bean 上的方法执行。 一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。 在 @AspectJ 中,一个切入点实际就是一个普通的方法定义提供的一个签名,并且切入点表达式使用 @Pointcut 注解来表示(这个方法的返回类型必须是 void)。

如下的例子定义了一个切入点'anyOldTransfer',这个切入点匹配了任意名为transfer的方法执行

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

切入点表达式,也就是 @Pointcut 注解的值,是正规的 AspectJ 5 切入点表达式。 如果你想要更多了解 AspectJ 的 切入点语言,请参见AspectJ 编程指南(如果要了解扩展请参阅 AspectJ 5 开发手册) 或者其他人写的关于 AspectJ 的书,例如 Colyer et. al.著的《Eclipse AspectJ》或者 Ramnivas Laddad著的《AspectJ in Action》。

支持的切入点指定者

Spring AOP 支持在切入点表达式中使用如下的 AspectJ pointcut designators (切入点指定者 PCD) :

其他的切入点类型

完整的 AspectJ 切入点语言支持额外的切入点指定者,但是 Spring 不支持这个功能。 他们分别是call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, 和 @withincode 。 在 Spring AOP 中使用这些指定者将会导致抛出IllegalArgumentException 异常。

在 Spring AOP 设置切入点指定者可能会在未来的发布中进行扩展,用来支持更多 AspectJ 切入点指定者。

  • execution - 匹配方法执行的连接点,这是你将会用到的 Spring 的最主要的切入点指定者。
  • within - 限定匹配特定类型的连接点(在使用 Spring AOP 的时候,在匹配的类型中定义的方法的执行)。
  • this - 限定匹配特定的连接点(使用 Spring AOP 的时候方法的执行),其中 bean reference(Spring AOP 代理)是指定类型的实例。
  • target - 限定匹配特定的连接点(使用 Spring AOP 的时候方法的执行),其中目标对象(被代理的 appolication object )是指定类型的实例。
  • args - 限定匹配特定的连接点(使用 Spring AOP 的时候方法的执行),其中参数是指定类型的实例。
  • @target - 限定匹配特定的连接点(使用 Spring AOP 的时候方法的执行),其中执行的对象的类已经有指定类型的注解。
  • @args - 限定匹配特定的连接点(使用 Spring AOP 的时候方法的执行),其中实际传入参数的运行时类型有指定类型的注解。
  • @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用 Spring AOP 的时候,所执行的方法所在类型已指定注解)。
  • @annotation - 限定匹配特定的连接点(使用 Spring AOP 的时候方法的执行),其中连接点的主题有某种给定的注解。

因为 Spring AOP 限制了连接点必须是方法执行级别的,pointcut designators 的讨论也给出了一个定义,这个定义和 AspectJ 的编程指南中的定义相比显得更加狭窄。 除此之外,AspectJ 它本身有基于类型的语义,在执行的连接点'this'和'target'都是指同一个对象,也就是执行方法的对象。 Spring AOP 是一个基于代理的系统,并且严格区分代理对象本身(对应于'this')和背后的目标对象(对应于'target')

由于 Spring 的 AOP 框架基于代理的本质,protected 方法通过定义拦截来实现的,无论是对 JDK 代理(这并不适用)还是 CGLIB 代理(这在技术上是可行的,但不值得推荐的用于AOP)。因此,任何给定的切入点将只匹配 public 方法!

如果您的拦截需求包括 protected/private 方法或者构造函数,考虑使用 Spring-driven 原生的 AspectJ 织入而不是 Spring 的基于代理的 AOP 框架。这就构成了一个 AOP 使用具有不同特点的不同模式,所以一定要在做决定之前首先要让自己熟悉织入。

Spring AOP 还支持另一个 PCD 命名的 bean。PCD 允许您限制连接点匹配的特定命名的 Spring bean,或者一组名为 Spring bean(当使用通配符)。bean PCD 有以下形式:

bean(idOrNameOfBean)

idOrNameOfBean 令牌是可以任何 Spring bean 的名称:使用 * 通配符支持字符提供限制,所以如果你为 Spring bean 建立一些命名约定可以很容易地编写一个bean PCD表达出来。一样与其他切入点指示器、bean PCD 可以是 &&'ed, ||'ed, 和 ! (否定)

注意:bean PCD 只在 Spring AOP 支持-而不在原生的 AspectJ 织入。这个是 Spring 特有对 AspectJ 定义的 PCD 的扩展

合并切入点表达式

切入点表达式可以使用 &&, || 和 ! 来合并.还可以通过名字来指向切入点表达式。 以下的例子展示了三种切入点表达式:anyPublicOperation(在一个方法执行连接点代表了任意 public 方法的执行时匹配); inTrading(在一个代表了在交易模块中的任意的方法执行时匹配) 和 tradingOperation(在一个代表了在交易模块中的任意的公共方法执行时匹配)。

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

就上所示的,从更小的命名组件来构建更加复杂的切入点表达式是一种最佳实践。 当用名字来指定切入点时使用的是常见的Java成员可视性访问规则。 (比如说,你可以在同一类型中访问私有的切入点,在继承关系中访问受保护的切入点,可以在任意地方访问公共切入点。 成员可视性访问规则不影响到切入点的 匹配。

共享常见的切入点定义

当开发企业级应用的时候,你通常会想要从几个切面来参考模块化的应用和特定操作的集合。 我们推荐定义一个“SystemArchitecture”切面来捕捉常见的切入点表达式。一个典型的切面可能看起来像下面这样:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

定义的切入点可以指任何一个切面,你需要一个切入点表达式。例如,服务层的事务,你可以写:

<aop:config>
    <aop:advisor
        pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
        advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

9.3. Schema-based AOP support中讨论 <aop:config><aop:advisor>标签。 在 12. Transaction Management 中讨论事务标签。

例子

Spring AOP 用户可能会经常使用 execution pointcut designator。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
        throws-pattern?)

除了返回类型模式,名字模式和参数模式以外,所有的部分都是可选的。 返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是 *,它代表了匹配任意的返回类型。 一个全称限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用*通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:() 匹配了一个不接受任何参数的方法, 而 (..) 匹配了一个接受任意数量参数的方法(零或者更多)。 模式 (*) 匹配了一个接受一个任何类型的参数的方法。 模式 (*,String) 匹配了一个接受两个参数的方法,第一个可以是任意类型,第二个则必须是String类型。 请参见 AspectJ编程指南的 Language Semantics 部分。

下面给出一些常见切入点表达式的例子。

任意 public 方法的执行:

execution(public * *(..))

任何一个以“set”开始的方法的执行:

execution(* set*(..))

AccountService 接口的任意方法的执行:

execution(* com.xyz.service.AccountService.*(..))

定义在service包里的任意方法的执行:

execution(* com.xyz.service.*.*(..))

定义在service包或者子包里的任意方法的执行:

execution(* com.xyz.service..*.*(..))

在service包里的任意连接点(在Spring AOP中只是方法执行) :

within(com.xyz.service.*)

在service包或者子包里的任意连接点(在Spring AOP中只是方法执行) :

within(com.xyz.service..*)

实现了 AccountService 接口的代理对象的任意连接点(在Spring AOP中只是方法执行) :

this(com.xyz.service.AccountService)

'this'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得代理对象可以在通知体内访问到的部分。

实现了 AccountService 接口的目标对象的任意连接点(在Spring AOP中只是方法执行) :

target(com.xyz.service.AccountService)

'target'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得目标对象可以在通知体内访问到的部分。

任何一个只接受一个参数,且在运行时传入的参数实现了 Serializable 接口的连接点 (在Spring AOP中只是方法执行)

args(java.io.Serializable)

'args'在binding form中用的更多:- 请常见以下讨论通知的章节中关于如何使得方法参数可以在通知体内访问到的部分。

请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable))只有在动态运行时候传入参数是可序列化的(Serializable)才匹配,而 execution 在传入参数的签名声明的类型实现了 Serializable 接口时候匹配。

有一个 @Transactional 注解的目标对象中的任意连接点(在Spring AOP 中只是方法执行)

@target(org.springframework.transaction.annotation.Transactional)

'@target' 也可以在binding form中使用:请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

任何一个目标对象声明的类型有一个 @Transactional 注解的连接点(在Spring AOP中只是方法执行)

@within(org.springframework.transaction.annotation.Transactional)

'@within'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

任何一个执行的方法有一个 @Transactional annotation的连接点(在Spring AOP中只是方法执行)

@annotation(org.springframework.transaction.annotation.Transactional)

'@annotation' 也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得annotation对象可以在通知体内访问到的部分。

任何一个接受一个参数,并且传入的参数在运行时的类型实现了 @Classified annotation的连接点(在Spring AOP中只是方法执行)

@args(com.xyz.security.Classified)

'@args'也可以在binding form中使用:- 请常见以下讨论通知的章节中关于如何使得 annotation 对象可以在通知体内访问到的部分。

任何 Spring bean 命名为 tradeService 的切入点(在Spring AOP中只是方法执行):

bean(tradeService)

任何 Spring bean 命名符合正则表达式为 *Service 的切入点(在Spring AOP中只是方法执行):

bean(*Service)

编写好的切入点

在编译过程中,AspectJ 执行切入点来尝试和优化匹配性能。检查代码确认每个连接点是否都匹配(静态或者动态)给出的切入点,这个代价是昂贵的。(动态匹配意味着匹配不能完全在静态分析上确认,而是用一个在代码运行时的代码测试来了来确认是否真实匹配了)。当第一次遇到切入点声明,AspectJ 将会重写一个最佳形式用于匹配执行。啥意思?基本切入点被重写为 DNF (Disjunctive Normal Form) ,切入点组件被分类,这样,评估检查代价更低。意味着无需担心不同切入点 designators(编号)之间的性能,可以任意顺序声明切入点来使用它们。

然而,AspectJ 只能工作于告诉他的事,为了优化匹配性能,你要考虑他们尝试获取和搜索空间来匹配可能的定义。现存的 designators(编号)主要有三类: kinded, scoping 和 context:

  • Kinded:选择一个特殊的 join point(连接点),如: execution, get, set, call, handler
  • Scoping:选择一个感兴趣的(很多类型的) join point(连接点)组如: within, withincode
  • Context:匹配(和可选绑定)基于上下文 ,如: this, target, @annotation

9.2.4 声明 advice

Advice 与切入点表单式相关联,并且与切入点匹配时执行 before, after, 或 around 方法。切入点表达式可以是一个命名切入点的引用,也可以是就地声明切入点表达式。

Before advice

Before advice 使用 @Before 注解来声明

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

如果是就地切入点表达式,上面例子可以重写为:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}

After returning advice

当匹配一个方法执行正常返回时,After returning advice 执行。声明使用 @AfterReturning 注解

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

注意:可以声明多个 advice

有时需要访问 advice 返回的真实值,可以使用 @AfterReturning 绑定到返回值:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning 属性对应了 advice 方法的参数名称,当方法执行返回,返回值将会传递给对于的 advice 方法的参数值。returning 条款还限制匹配只有那些返回一个指定类型值的方法执行(在本例中Object,它将匹配任何返回值)。

注意:当使用 after-returning advice,不可能返回一个完全不同的引用。

After throwing advice

当匹配到方法执行抛出异常时,After throwing advice 执行。使用 @AfterThrowing 注解:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

Often you want the advice to run only when exceptions of a given type are thrown, and you also often need access to the thrown exception in the advice body. Use the throwing attribute to both restrict matching (if desired, use Throwable as the exception type otherwise) and bind the thrown exception to an advice parameter.